spb/chat-spboucher-ai
Public
1import { guarded, json } from "@/lib/api";2import { sql } from "@/lib/db";3import { Document, Packer, Paragraph, HeadingLevel, TextRun, ExternalHyperlink } from "docx";45export const GET = guarded<{ id: string }>(async (req, { params }) => {6 const r = (await sql`SELECT * FROM research_runs WHERE id = ${params.id}`)[0];7 if (!r) return json({ error: "Introuvable" }, { status: 404 });8 const fmt = req.nextUrl.searchParams.get("format");9 const safe = String(r.question).replace(/[^\w àâçéèêëîïôûùüÿœ-]/gi, "_").slice(0, 50);10 if (fmt === "md") return new Response(`# ${r.question}\n\n_Rapport de recherche — ${r.model_id} — ${new Date(r.created_at).toLocaleString("fr-CA")}_\n\n${r.report_md ?? ""}`, { headers: { "content-type": "text/markdown; charset=utf-8", "content-disposition": `attachment; filename="${safe}.md"` } });11 if (fmt === "docx") {12 const paras: Paragraph[] = [new Paragraph({ text: String(r.question), heading: HeadingLevel.TITLE })];13 for (const line of String(r.report_md ?? "").split("\n")) {14 const h = line.match(/^(#{1,4})\s+(.*)$/);15 if (h) paras.push(new Paragraph({ text: h[2], heading: [HeadingLevel.HEADING_1, HeadingLevel.HEADING_2, HeadingLevel.HEADING_3, HeadingLevel.HEADING_4][h[1].length - 1] }));16 else if (/^\s*[-*]\s+/.test(line)) paras.push(new Paragraph({ children: inline(line.replace(/^\s*[-*]\s+/, "")), bullet: { level: 0 } }));17 else if (line.trim()) paras.push(new Paragraph({ children: inline(line) }));18 }19 const doc = new Document({ sections: [{ children: paras }] });20 const buf = await Packer.toBuffer(doc);21 return new Response(new Uint8Array(buf), { headers: { "content-type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "content-disposition": `attachment; filename="${safe}.docx"` } });22 }23 return json({ run: r });24});2526function inline(text: string): (TextRun | ExternalHyperlink)[] {27 const out: (TextRun | ExternalHyperlink)[] = [];28 const re = /(\*\*[^*]+\*\*|\[[^\]]+\]\((https?:\/\/[^)]+)\)|https?:\/\/\S+)/g;29 let last = 0; let m: RegExpExecArray | null;30 while ((m = re.exec(text))) {31 if (m.index > last) out.push(new TextRun(text.slice(last, m.index)));32 const tok = m[0];33 if (tok.startsWith("**")) out.push(new TextRun({ text: tok.slice(2, -2), bold: true }));34 else if (tok.startsWith("[")) { const t = tok.match(/^\[([^\]]+)\]\((.*)\)$/)!; out.push(new ExternalHyperlink({ link: t[2], children: [new TextRun({ text: t[1], style: "Hyperlink" })] })); }35 else out.push(new ExternalHyperlink({ link: tok, children: [new TextRun({ text: tok, style: "Hyperlink" })] }));36 last = m.index + tok.length;37 }38 if (last < text.length) out.push(new TextRun(text.slice(last)));39 return out;40}4142export const DELETE = guarded<{ id: string }>(async (_req, { params }) => { await sql`DELETE FROM research_runs WHERE id = ${params.id}`; return json({ ok: true }); });43